﻿using System;
using System.Globalization;
using System.IO;
using System.Net;

namespace Hack.Net.Downloader
{
    /// <summary>
    /// Provides instance methods for the starting and resuming the download of a file.
    /// This class cannot be inherited.  
    /// </summary>
    internal sealed class DownloadInfo : IDisposable
    {

        #region Fields
        private ICredentials credentials;
        private bool disposed;
        private bool downloadHtml;
        private Stream downloadStream;
        private long length;
        private bool preAuthenticate;
        private IWebProxy proxy;
        private WebResponse response;
        private long start;
        private string userAgent = null;
        #endregion

        #region Private Methods

        #region CheckDisposed
        /// <summary>
        /// Protects against the <see cref="DownloadInfo"/> from being used
        /// after it has been Disposed.
        /// </summary>
        private void CheckDisposed()
        {
            if (this.Disposed)
            {
                throw new ObjectDisposedException(base.GetType().FullName);
            }
        }
        #endregion

        #region Dispose
        /// <summary>
        /// Releases the resources, other than memory, that are used by the <see cref="DownloadInfo"/>. 
        /// </summary>
        /// <param name="disposing"><b>true</b> to release both managed and unmanaged resources; <b>false</b> to release only unmanaged resources.</param>
        private void Dispose(bool disposing)
        {
            // Dispose(bool disposing) executes in two distinct scenarios. If 
            // disposing equals true, the method has been called directly or
            // indirectly by a user's code. Managed and unmanaged resources 
            // can be disposed.
            //
            // If disposing equals false, the method has been called by the 
            // runtime from inside the finalizer and you should not reference 
            // other objects. Only unmanaged resources can be disposed.
            //
            // Note that this is not thread safe. Another thread could start 
            // disposing the object after the managed resources are disposed,
            // but before the disposed flag is set to true. If thread safety 
            // is necessary, it must be implemented by the caller.

            if (!disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources.
                    if (this.downloadStream != null && this.downloadStream != Stream.Null)
                    {
                        this.downloadStream.Close();
                    }

                    if (this.response != null)
                    {
                        this.response.Close();
                    }
                }

                // There are no unmanaged resources to release, but
                // if we add them, they need to be released here.
            }
            disposed = true;
        }
        #endregion

        #region GetFileSize
        /// <summary>
        /// Gets the size of the file being downloaded.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file being downloaded.</param>
        /// <returns>The size of the file in bytes. If the size cannot be
        /// determined, a -1 is returned.</returns>
        private long GetFileSize(Uri url)
        {
            long size = -1;

            using (WebResponse response = GetRequest(url).GetResponse())
            {
                size = response.ContentLength;
            }

            return size;
        }
        #endregion

        #region GetRequest
        /// <summary>
        /// Creates a <see cref="WebRequest"/> corresponding to the
        /// <see cref="Uri"/> specified.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> used to create the request.</param>
        /// <returns>A <see cref="WebRequest"/> corresponding to 
        /// <see cref="Uri"/>.</returns>
        private WebRequest GetRequest(Uri url)
        {
            WebRequest request = WebRequest.Create(url);

            HttpWebRequest webRequest = request as HttpWebRequest;
            if (webRequest != null)
            {
                request.Credentials = this.Credentials;
                request.PreAuthenticate = this.PreAuthenticate;

                // The default for the HttpWebRequest is for this value to
                // be null, so only do this if the UserAgent value is not
                // null or empty.
                if (!String.IsNullOrEmpty(this.UserAgent))
                {
                    webRequest.UserAgent = this.UserAgent;
                }
            }
            request.Proxy = this.Proxy;
            return request;
        }

        #endregion

        #region GetServerLastModified
        /// <summary>
        /// Gets the last modified date of the file from the server.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file.</param>
        /// <returns>The <see cref="DateTime"/> the file was last modified on
        /// the server, in Universal Time. If the last modified date is not 
        /// able to be determined, a <see langword="null"/> is returned.</returns>
        /// <remarks>If the <see cref="Uri"/> specified uses the HTTP or HTTPS
        /// schemes, the last modified date is what is returned in the
        /// <see cref="HttpWebResponse"/>. If the <see cref="Uri"/> uses the
        /// FTP scheme, the last modified date is returned from the 
        /// <see cref="WebRequestMethods.Ftp.GetDateTimestamp"/> request
        /// method.</remarks>
        private DateTime? GetServerLastModified(Uri url)
        {
            DateTime? lastModified = null;
            WebRequest request = null;

            if (url.Scheme == Uri.UriSchemeHttp || url.Scheme == Uri.UriSchemeHttps)
            {
                request = GetRequest(url) as HttpWebRequest;
                if (request != null)
                {
                    using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
                    {
                        // Check to make sure the response isn't an error. If it is this method
                        // will throw exceptions.
                        ValidateResponse(response, url);

                        lastModified = response.LastModified.ToUniversalTime();
                    }
                }
            }
            else if (url.Scheme == Uri.UriSchemeFtp)
            {
                request = GetRequest(url) as FtpWebRequest;
                if (request != null)
                {
                    request.Method = WebRequestMethods.Ftp.GetDateTimestamp;

                    using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
                    {
                        // Check to make sure the response isn't an error. If it is this method
                        // will throw exceptions.
                        ValidateResponse(response, url);

                        lastModified = response.LastModified.ToUniversalTime();
                    }
                }
            }
            return lastModified;
        }
        #endregion

        #region ValidateResponse
        /// <summary>
        /// Validates the response recieved.
        /// </summary>
        /// <param name="response">The <see cref="WebResponse"/> received.</param>
        /// <param name="url">The <see cref="Uri"/> of the request.</param>
        /// <exception cref="ArgumentException">
        /// <para>If the response received is an
        /// <see cref="HttpWebResponse"/> that contains HTML data and the
        /// <see cref="DownloadHtml"/> property is <see langword="false"/>.</para>
        /// <para>-or-</para>
        /// <para>The response recieved is an <see cref="FtpWebResponse"/> and the
        /// <see cref="FtpWebResponse.StatusCode"/> is 
        /// <see cref="FtpStatusCode.ConnectionClosed"/>.</para>
        /// </exception>
        private void ValidateResponse(WebResponse response, Uri url)
        {
            if (response.GetType() == typeof(HttpWebResponse))
            {
                HttpWebResponse httpResponse = response as HttpWebResponse;

                // If it's an HTML page, it's probably an error page. If we aren't
                // allowing HTML pages to be downloaded, throw the error.
                if (httpResponse.ContentType.Contains("text/html") || httpResponse.StatusCode == HttpStatusCode.NotFound)
                {
                    if (!this.DownloadHtml)
                    {
                        throw new ArgumentException("Resources.DownloadInfoInvalidResponseReceived" + url.ToString());
                    }
                }
            }
            else if (response.GetType() == typeof(FtpWebResponse))
            {
                FtpWebResponse ftpResponse = response as FtpWebResponse;
                if (ftpResponse.StatusCode == FtpStatusCode.ConnectionClosed)
                    throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "DownloadInfoConnectionClosed" + url.ToString()));
            }
            // FileWebResponse doesn't have a status code to check.
        }
        #endregion

        #endregion

        #region Properties

        #region Credentials
        /// <summary>
        /// Gets or sets the network credentials used for authenticating the the request with the Internet resource.
        /// </summary>
        /// <value>The <see cref="ICredentials"/> containing the authentication credentials associated with the request. 
        /// The default value is a <see langword="null"/>.</value>
        /// <remarks>The <see cref="Credentials"/> property contains the authentication credentials required to access
        /// the Internet resource..
        /// </remarks>
        public ICredentials Credentials
        {
            get
            {
                this.CheckDisposed();
                return this.credentials;
            }
            set
            {
                this.CheckDisposed();
                this.credentials = value;
            }
        }
        #endregion

        #region Disposed
        /// <summary>
        /// Gets a value that indicates whether the provider is disposed.
        /// </summary>
        /// <value><b>true</b> if the object is disposed; otherwise, <b>false</b>. </value>
        public bool Disposed
        {
            get
            {
                lock (this)
                {
                    return disposed;
                }
            }
        }
        #endregion

        #region DownloadHtml
        /// <summary>
        /// Gets or sets a value indicating if HTML pages should be allowed
        /// to be downloaded.
        /// </summary>
        /// <value><see langowrd="true"/> to download HTML pages; otherwise,
        /// <see langword="false"/>.</value>
        public bool DownloadHtml
        {
            get
            {
                this.CheckDisposed();
                return this.downloadHtml;
            }
            set
            {
                this.CheckDisposed();
                this.downloadHtml = value;
            }
        }
        #endregion

        #region DownloadStream
        /// <summary>
        /// Gets the underlying <see cref="System.IO.Stream"/> associated with the download.
        /// </summary>
        /// <value>A <see cref="System.IO.Stream"/> or a <see cref="System.IO.Stream.Null"/>.</value>
        public Stream DownloadStream
        {
            get
            {
                this.CheckDisposed();
                if (this.start == this.length)
                {
                    return Stream.Null;
                }

                if (this.downloadStream == null)
                {
                    this.downloadStream = this.response.GetResponseStream();
                }

                return this.downloadStream;
            }
        }
        #endregion

        #region IsProgressKnown
        /// <summary>
        /// Gets a value indicating if download progress information is able to be determined.
        /// </summary>
        /// <value>A <see cref="System.Boolean"/> value indicating if the progress information can be determined.</value>
        public bool IsProgressKnown
        {
            get
            {
                // If the size of the remote url is -1, that means we
                // couldn't determine it, and so we don't know
                // progress information.
                this.CheckDisposed();
                return this.length > -1;
            }
        }
        #endregion

        #region Length
        /// <summary>
        /// Gets the size of the current file.
        /// </summary>
        /// <value>The size of the current file.</value>
        /// <remarks>This property value is a <see langword="null"/> if the file system containing the file does not support this information.</remarks>
        public long Length
        {
            get
            {
                this.CheckDisposed();
                return this.length;
            }
        }
        #endregion

        #region PreAuthenticate
        /// <summary>
        /// Indicates whether to pre-authenticate the request.
        /// </summary>
        /// <value><see langword="true"/> to pre-authenticate; otherwise,
        /// <see langword="false"/>.</value>
        /// <remarks>With the exception of the first request, the 
        /// <see cref="PreAuthenticate"/> property indicates whether to send
        /// authentication information with subsequent requests without waiting
        /// to be challenged by the server. When <see cref="PreAuthenticate"/>
        /// is <see langword="false"/>, the <see cref="WebRequest"/> waits
        /// for an authentication challenge before sending authentication
        /// information.</remarks>
        public bool PreAuthenticate
        {
            get
            {
                this.CheckDisposed();
                return this.preAuthenticate;
            }
            set
            {
                this.CheckDisposed();
                this.preAuthenticate = value;
            }
        }
        #endregion

        #region Proxy
        /// <summary>
        /// Gets or sets proxy information for the request.
        /// </summary>
        /// <value>The <see cref="IWebProxy"/> object to use to proxy the request. The default value is set to the <see cref="WebRequest.DefaultWebProxy"/> property. </value>
        /// <remarks>The <see cref="Proxy"/> property identifies the WebProxy object to use to process requests to Internet
        /// resources. To specify that no proxy should be used, set the Proxy property to the proxy instance returned by 
        /// the <see cref="GlobalProxySelection.GetEmptyWebProxy"/> method.
        /// </remarks>
        public IWebProxy Proxy
        {
            get
            {
                this.CheckDisposed();
                return this.proxy;
            }
            set
            {
                this.CheckDisposed();
                this.proxy = value;
            }
        }
        #endregion

        #region Response
        /// <summary>
        /// Gets the <see cref="System.Net.WebResponse"/> associated with the download.
        /// </summary>
        /// <value>The <see cref="System.Net.WebResponse"/> associated with the download.</value>
        public WebResponse Response
        {
            get
            {
                this.CheckDisposed();
                return response;
            }
        }
        #endregion

        #region StartPoint
        /// <summary>
        /// Gets the starting byte offset in the destination file.
        /// </summary>
        /// <value>A <see cref="Int64"/> value indicating the starting offset in the destination file.</value>
        public long StartPoint
        {
            get
            {
                this.CheckDisposed();
                return this.start;
            }
        }
        #endregion

        #region UserAgent
        /// <summary>
        /// Gets or sets the value of the <b>User-agent</b> HTTP header. 
        /// </summary>
        /// <value>The value of the <b>User-agent</b> HTTP header. The default
        /// value is <see langword="null"/>.</value>
        public string UserAgent
        {
            get
            {
                this.CheckDisposed();
                return this.userAgent;
            }
            set
            {
                this.CheckDisposed();
                this.userAgent = value;
            }
        }
        #endregion
        #endregion

        #region Methods

        #region constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="DownloadInfo"/> class.
        /// </summary>
        public DownloadInfo()
        {
            // Set the default web proxy
            this.proxy = WebRequest.DefaultWebProxy;
        }
        #endregion

        #region Close
        /// <summary>
        /// Close the underlying <see cref="System.Net.WebResponse"/>.
        /// </summary>
        public void Close()
        {
            this.Dispose(true);

            // Take ourself off the finalization queue to prevent
            // finalization from executing a second time.
            GC.SuppressFinalize(this);
        }
        #endregion

        #region Dispose
        /// <summary>
        /// Releases all resources used by the <see cref="DownloadInfo"/>.
        /// </summary>
        public void Dispose()
        {
            this.Close();
        }
        #endregion

        #region GetLastModifiedDate
        /// <summary>
        /// Retrieves the last modified date of the file at the specified url.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file.</param>
        /// <returns>The <see cref="DateTime"/> the file was last modified on
        /// the server, in Universal Time. If the last modified date is not 
        /// able to be determined, a <see langword="null"/> is returned.</returns>
        public DateTime? GetLastModifiedDate(Uri url)
        {
            this.CheckDisposed();

            return GetServerLastModified(url);
        }
        #endregion

        #region Initialize
        /// <summary>
        /// Initializes a new instance of a <see cref="DownloadInfo"/> class.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        /// <param name="destintationFolder">The destination folder for the download.</param>
        /// <returns>A <see cref="DownloadInfo"/> that contains the information for the file to be downloaded.</returns>
        public void Initialize(Uri url, string destintationFolder)
        {
            this.CheckDisposed();
            long urlSize = GetFileSize(url);
            this.length = urlSize;

            WebRequest req = GetRequest(url);
            this.response = (WebResponse)req.GetResponse();

            // Check to make sure the response isn't an error. If it is this method
            // will throw exceptions.
            ValidateResponse(this.response, url);

            // Take the name of the file given to use from the web server.
            String fileName = System.IO.Path.GetFileName(this.response.ResponseUri.ToString());
            String downloadTo = Path.Combine(destintationFolder, fileName);

            // If we don't know how big the file is supposed to be,
            // we can't resume, so delete what we already have if something is on disk already.
            if (!this.IsProgressKnown && File.Exists(downloadTo))
            {
                File.Delete(downloadTo);
            }

            if (this.IsProgressKnown && File.Exists(downloadTo))
            {
                // We only support resuming on http requests
                if (!(this.Response is HttpWebResponse))
                {
                    File.Delete(downloadTo);
                }
                else
                {
                    // Try and start where the file on disk left off
                    this.start = new FileInfo(downloadTo).Length;

                    // If we have a file that's bigger than what is online, then something 
                    // strange happened. Delete it and start again.
                    if (this.start > urlSize)
                    {
                        File.Delete(downloadTo);
                        this.start = 0;
                    }
                    else if (this.start == urlSize)
                    {
                        // The file already exists, so check to see if the version on the
                        // server has a newer date. If we can't get the last modified date
                        // from the server we will get a null value back. In that case,
                        // we leave the existing file alone and don't download it.
                        DateTime? lastModifiedServer = this.GetServerLastModified(url);
                        if (lastModifiedServer.HasValue)
                        {
                            DateTime lastModifiedLocal = File.GetLastWriteTime(downloadTo).ToUniversalTime();
                            if (lastModifiedServer > lastModifiedLocal)
                            {
                                File.Delete(downloadTo);
                                this.start = 0;
                            }
                        }
                    }
                    else if (this.start < urlSize)
                    {
                        // Try and resume by creating a new request with a new start position
                        this.response.Close();
                        req = GetRequest(url);
                        ((HttpWebRequest)req).AddRange((int)this.start);
                        this.response = req.GetResponse();

                        if (((HttpWebResponse)this.Response).StatusCode != HttpStatusCode.PartialContent)
                        {
                            // They didn't support our resume request. 
                            File.Delete(downloadTo);
                            this.start = 0;
                        }
                    }
                }
            }
        }
        #endregion

        #endregion

    }
}
